# -*- coding: utf-8 -*-
#!/usr/bin/env python

import os
import sys
import wsgiref.handlers
import webapp2
import json
import time
import logging
import string
import math
import random
import urlparse
import urllib
import cgi
import jinja2
import copy

from datetime import datetime, timedelta
from google.appengine.api import memcache
from google.appengine.ext.deferred import defer
from google.appengine.ext import blobstore
from google.appengine.ext.webapp import blobstore_handlers
from google.appengine.datastore.datastore_query import Cursor
from google.appengine.api import urlfetch
from gaesessions import get_current_session
from operator import itemgetter

import statistics as StatUtil
from utils import *
from events import *
from models import *
from app_global import *
from game_functions import *

class SessionHandler(webapp2.RequestHandler):
	def session_user_id(self):
		session = get_current_session()
		if not session.is_active():
			return None

		return session.get('user_id')

	def before_filter(self):
		while (True):
			session = get_current_session()
			if not session.is_active():
				break
			userId = session.get('user_id')
			if userId is None:
				break
			user = User.get_by_id(userId)
			if user is None:
				break
			if (user.session_stamp is None) or (session.get('user_stamp')!=user.session_stamp):
				break
			params = json.loads(aes256_decrypt(self.request.body)) if self.request.body else {}
			self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
			yield params, user, session
			return
		self.error(401) # Unauthorized
		return

class InitHandler(webapp2.RequestHandler):
	def checkAppVersion(self, platform, version):
		# 앱 버전 체크
		platform = Platform.get_by_id(platform, parent=Platform.ancestorKey())
		if version < platform.version:
			result = {
				"result":1,
				"update":{"latest-version":platform.version, "url":platform.url}
			}
			self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
			self.response.write(json_dumps_encrypt(result))
			return False
		return True
	
	def checkAssetVersion(self, platform, version):
		maxAsset = Asset.query(ancestor=Asset.ancestorKey()).order(-Asset.version).get()
		if maxAsset and version < maxAsset.version:
			size, assetPatches = Asset.getPatches(version, platform)
			result = {
				"result":2,
				"assetPatchSize":size,
				"assetVersion":maxAsset.version,
				"assetPatches":assetPatches
			}
			self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
			self.response.write(json_dumps_encrypt(result))
			return False
		return True
		
	def post(self):
		params = json.loads(aes256_decrypt(self.request.body))

		if self.checkAppVersion(params['platform'], params['applicationVersion'])==False:
			return
		if self.checkAssetVersion(params['platform'], params['assetVersion'])==False:
			return

		# 성공 : 유저약관 적용
		result = {
			"result":0,
			"terms":"http://terms.funigloo.com/terms_html.php",
		}
		self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
		self.response.write(json_dumps_encrypt(result))

class LoginHandler(InitHandler):
	def checkAuthor(self, params, debug=False):
		if debug:
			return True
		getParams = {
			"access_token": params['accessToken'],
			"user_id": params['id'],
			"client_id": KAKAO_CLIENT_ID,
			"sdkVer":params['sdkVersion']
		}
		try:
			result = urlfetch.fetch(url="https://api.kakao.com/v1/token/check.json?"+urllib.urlencode(getParams), payload=None, method=urlfetch.GET)
		except DeadlineExceededError as de:
			logging.info('! skip verifying author(DeadlineExceededError) %s' % params['id'])
			return True
		except Exception as e:
			logging.info('! skip verifying author(DownloadError) %s, %s' % (params['id'],e,))
			return True
		
		if result.status_code==200:
			body = json.loads(result.content)
			if body['status']==0:
				return True
		# TODO : Add abuser
		logging.info('debug  %s, %s, %s' % (params['id'],result.content,result.status_code,))
		return False
		
	def post(self):
		params = json.loads(aes256_decrypt(self.request.body))

		if self.checkAuthor(params, self.app.debug)==False:
			self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
			self.response.write(json_dumps_encrypt({"result":4}))
			return

		season = Season.season()
		
		user = User.get_by_id(params['id'])
		if not user:
			user = User(id=params['id'])
			user.season = season.count
			user.mission_record.initializeAll(Mission.todayMission())
			user.accrue_record.rank_ups = [0,0,0]
			user.invite_rewards = [False, False, False, False]			
			newbie = True
		else:
			newbie = False
			# 개발중, 모델링이 바꼈을 경우에 호환을 하는 부분이므로 아래는 나중에 제거해야 하는 소스임.			
			if len(user.invite_rewards)<4:
				user.invite_rewards = [False, False, False, False]
			if len(user.mission_record.dplays)<GAME_COUNT:
				user.mission_record.initializeAll(Mission.todayMission())
			if len(user.mission_record.daily_statuses)<3:
				user.mission_record.daily_statuses = [-1]*3
				user.mission_record.weekly_statuses = [-1]*3
			dms = getStorageData("daily-missions") 
			for reward in user.accrue_record.achieve_rewards:
				if abs(reward)>len(dms):
					user.accrue_record.achieve_rewards = []
					break

		# for dynamic appending new-games
		while len(user.season1.ranks)<GAME_COUNT:
			user.season1.ranks.append(0)
		while len(user.season1.scores)<GAME_COUNT:
			user.season1.scores.append(0)
		while len(user.season2.ranks)<GAME_COUNT:
			user.season2.ranks.append(0)
		while len(user.season2.scores)<GAME_COUNT:
			user.season2.scores.append(0)		
		while len(user.mission_record.dplays)<GAME_COUNT:
			user.mission_record.dplays.append(0)
		while len(user.mission_record.wplays)<GAME_COUNT:
			user.mission_record.wplays.append(0)
				
		if self.checkAppVersion(params['platform'], params['applicationVersion'])==False:
			UserLog.append(user, ACTION_APP_UPDATE, {"platform":params['platform'], "version":params['applicationVersion']})
			return
		if self.checkAssetVersion(params['platform'], params['assetVersion'])==False:
			UserLog.append(user, ACTION_ASSET_UPDATE, {"platform":params['platform'], "version":params['assetVersion']})
			return
			
		userRestricted = UserRestricted.allRecords(user).order(-UserRestricted.expired_at).fetch(1, read_policy=ndb.EVENTUAL_CONSISTENCY)
		if userRestricted and userRestricted[0].expired_at > datetime.now():
			result = {
				"result":4,
				"msg":userRestricted[0].message,
				"until":replace_timezone(userRestricted[0].expired_at,'Asia/Seoul').strftime('%Y-%m-%d %H:%M:%S')
			}
			self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
			self.response.write(json_dumps_encrypt(result))
			UserLog.append(user, ACTION_RESTRICTED, {"msg":userRestricted[0].message, "until":result["until"]})
			
			session = get_current_session()
			if session.is_active():
				session.terminate()
			return
		
		user.platform_id = params['platform']
		user.application_version = params['applicationVersion']
		user.asset_version = params['assetVersion']
		user.name = cgi.escape(params['name']) if params.get('name') else None
		user.image_url = params.get('imageUrl')
		user.push_token = shrink_push_token(params.get('pushToken'))
		
		update_push_token(user)
		update_bout(user)

		session = get_current_session()
		if session.is_active():
			session.terminate()

		dailyBonusType, dailyBonusAmount = dailyLoginBonus(user, season)
		missions = []
		
		if dailyBonusType is not None:
			obtainReward(user, dailyBonusType, dailyBonusAmount)
						
			missions = checkMission(user, params, TRIGGER_LOGIN)
			for mission in missions:
				obtainReward(user, mission["reward"], mission["amount"])

		user.session_stamp = int(datetime.now().strftime('%Y%m%d%H%M%S'))
		user.put()
		
		session = get_current_session()
		session['user_id'] = user.key.id()
		session['user_stamp'] = user.session_stamp
		memcache.set('%s:newInbox' % user.key.id(), None)
		
		now = datetime.now()
		
		friendIds = params['friends']
		friendUsers = ndb.get_multi([ndb.Key(User, i) for i in params['friends']], read_policy=ndb.EVENTUAL_CONSISTENCY)
		friends, rank, existCount = getUserFriends(user, friendUsers, friendIds, season.count, False)
				
		seasonData = user.seasonData(season.count)
		
		EventFactory.run(user, session, params, TRIGGER_LOGIN)
		user.put()
			
		result = {
			"result":0,
			"seasonUpdated":user.seasonUpdated(season),
			"feedBoutInSec":user.feedBoutInSec(),
			"weeklyExpireInSec":season.weeklyExpireInSec(now),
			"inbox":UserInbox.unreadCount(user)>0,
			"notice":getNotices(),
			"newbie":newbie,
			"user": {
				"id":user.key.id(),
				"name":user.name,
				"imageUrl":user.image_url,
				"cash":user.cash,
				"coin":user.coin,
				"bout":user.bout,				
				"pushBlocked":user.push_blocked,
				"boutBlocked":user.bout_blocked,
				"score":sum(seasonData.scores),
				"scores":seasonData.scores,
				"bestScore":user.accrue_record.best_score,
				"invite":user.accrue_record.invite,
				"remainInvite":user.remainiInvite(user.accrue_record.invite),
				"inviteRewards":user.invite_rewards,
				"rankUps":user.accrue_record.rank_ups,				
				"tutorials":user.tutorials,
			},			
			"friends":friends,			
			"favorates":[g for g in range(GAME_COUNT)], # [0,1,2,3,4] 가 디폴트고, 관리자에 의해 바뀔 수 있음. (소스 수정 후 서버 패치)
			"alerts":popAlerts(user),
			"missions":missions,
			"review": {
				"done":user.review,
				"url":DOMAIN+"/review?id={0}".format(user.key.id())
			}
		}
		
		if dailyBonusType is not None:
			result["stamp"] = {
				"day":user.daily_stamp,
				"type":dailyBonusType,
				"amount":dailyBonusAmount,
			}
		
		#if user.application_version==0 and user.platform_id==PLATFORM_IOS:	# remove this after passed review.
		#	result["onReview"] = True
		
		self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
		self.response.write(json_dumps_encrypt(result))
		
		if dailyBonusType is not None:
			DailyActiveUser.append(user)
			StatUtil.increment_day(STAT_DAU.format(user.platform_id))
		if newbie:
			StatUtil.increment(STAT_REGISTER.format(user.platform_id))
		StatUtil.increment(STAT_LOGIN.format(user.platform_id))
		
		UserLog.append(user, ACTION_REGISTER if newbie else ACTION_LOGIN, 
			{
				"platform":user.platform_id, 
				"cash":user.cash, 
				"coin":user.coin, 
				"bout":user.bout, 
				"udpoint":0,
				"stamp":user.daily_stamp,
				"blocks":[user.push_blocked,user.bout_blocked], 
				"score":sum(seasonData.scores),
				"rank":rank,
			}
		)

class ShopHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			if EventFactory.run(user, session, params, TRIGGER_LOGIN) > 0:
				user.put()
			
			result = {
				"cashes":getStorageData("cashes"),
				"coins":getStorageData("coins"),
				"bouts":getStorageData("bouts"),
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			self.response.write(json_dumps_encrypt(result))
			
class AppFriendsHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			season = Season.season()
			friendIds = params['friends']
			friendUsers = ndb.get_multi([ndb.Key(User, i) for i in params['friends']], read_policy=ndb.EVENTUAL_CONSISTENCY)
			friends, rank, existCount = getUserFriends(user, friendUsers, friendIds, season.count, False)
			
			seasonData = user.seasonData(season.count)
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			user.put()
			
			result = {
				"user":{
					"score":sum(seasonData.scores),
					"scores":seasonData.scores,
				},
				"friends":friends,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			self.response.write(json_dumps_encrypt(result))
			
			UserLog.append(user, ACTION_APPFRIENDS, 
				{
					"rank":rank, 
					"friendCount":len(friends), 
				}
			)

class NoAppFriendsHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			session = get_current_session()
			current = "".join(str(f) for f in params['friends']).__hash__()
			old = session.get('hash:noappFriendsHash')
			if (old is None) or (current != old):
				memcache.set("%s:noappFriends" % user.key.id(), None)
				session['hash:noappFriendsHash'] = current
			
			friends = memcache.get("%s:noappFriends" % user.key.id())
			if friends:
				return self.response.write(json_dumps_encrypt({ "friends":friends }))
			
			now = datetime.now()
			userInvites = UserInvite.query(UserInvite.user_id==user.key.id(), ancestor=UserInvite.ancestorKey(user)).fetch(read_policy=ndb.EVENTUAL_CONSISTENCY)
			
			friends=[]
			
			for fid in params['friends']:
				friend = None
				for ui in userInvites:
					if ui.friend_id==abs(fid):
						friend = {
							"id":fid,
							"status":1 if (INTERVAL_INVITE - int(math.ceil((now - ui.updated_at).total_seconds()))>0) else 0,
						}
						userInvites.remove(ui)
						break
				
				if friend is None:
					friend = {
						"id":fid,
						"status":0,
					}
				friends.append(friend)

			memcache.set("%s:noappFriends" % user.key.id(), friends, 60*5)
			
			self.response.write(json_dumps_encrypt({ "friends":friends }))
			
class ReadyBuyCashHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():	
			cash = getStorageData("cashes")[params['id']-1]
			if cash["current_amount"]!=params['cash']: # mismatch (sync discount-time)
				self.response.write(json_dumps_encrypt({"result":1}))
				return

			userPurchase = UserPurchase(user_id=user.key.id(), cash_id=cash["id"], cash=cash["current_amount"]).put()

			user.payload = userPurchase.id()
			user.put()

			result = {
				"result":0,
				"id":params['id'],
				"payload":user.payload,
			}
			self.response.write(json_dumps_encrypt(result))
			
			UserLog.append(user, ACTION_READY_IAP, 
				{
					"id":params['id'],
					"cash":cash["current_amount"],
					"payload":user.payload,
				}
			)	
	
class BuyCashHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():	
			payload = params['payload'] # aes256_decrypt(params['payload'])
			userPurchase = UserPurchase.get_by_id(int(payload))

			if userPurchase is None:
				self.error(400) # Bad Request
				Abuser.addUser(user, ABUSE_IAP_HACK)
				UserLog.append(user, ACTION_IAP, 
					{
						"status":u"payload 찾을 수 없음(어뷰징)",
						"cash":0,
						"ucash":user.cash
					}
				)
				return

			if userPurchase.status!=None:
				self.error(400) # Bad Request
				Abuser.addUser(user, ABUSE_IAP_HACK)
				UserLog.append(user, ACTION_IAP, 
					{
						"status":u"이미 사용된 payload(어뷰징)",
						"cash":0,
						"ucash":user.cash
					}
				)
				return

			result = {}

			verified = False
			if user.platform_id==PLATFORM_IOS:
				verified = verifyIOSReceipt(user, params['receipt'], APPSTORE_BID, bool(params.get('debug')))
			elif user.platform_id==PLATFORM_PLAYSTORE:
				verified = verifyPlayStoreReceipt(user, params['receipt'], params['signature'])

			if not verified:
				userPurchase.status=None
				userPurchase.receipt = str(params.get('receipt'))
				userPurchase.signature = str(params.get('signature'))
				userPurchase.put()
				self.error(400) # Bad Request
				
				logging.info("receipt=%s" % params.get('receipt'))
				logging.info("signature=%s" % params.get('signature'))
				logging.info("payload=%s" % params.get('payload'))
				
				Abuser.addUser(user, ABUSE_IAP_FAIL)
				UserLog.append(user, ACTION_IAP, 
					{
						"status":u"검증실패(어뷰징)",
						"cash":userPurchase.cash,
						"ucash":user.cash,
						"signature":params.get('signature'),
					}
				)
				return

			userPurchase.receipt = str(params['receipt'])
			userPurchase.put()

			user.cash += userPurchase.cash
			user.put()

			cash = getStorageData("cashes")[userPurchase.cash_id-1]
			PRICE = 0
			if user.platform_id==PLATFORM_IOS:
				defer(send_purcase_to_kakao, user.key.id(), user.platform_id, userPurchase.key.id(), cash["USD"], "USD")
				PRICE = int(math.ceil(cash["USD"]))
			else:
				defer(send_purcase_to_kakao, user.key.id(), user.platform_id, userPurchase.key.id(), cash["KRW"], "KRW")
				PRICE = cash["KRW"]
			
			result = {
				"cash":user.cash,
				"result":0,
				"alerts":popAlerts(user),
				"dailyPoint":0,
			}
			
			self.response.write(json_dumps_encrypt(result))
			
			StatUtil.increment(STAT_BUY_CASH.format(user.platform_id, userPurchase.cash_id), PRICE)
			
			UserLog.append(user, ACTION_IAP, 
				{
					"status":u"성공",
					"cash":userPurchase.cash,
					"friend":params.get('friendId'),
					"ucash":user.cash,
					"udpoint":0,
				}
			)

class BuyCoinHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			coin = getStorageData("coins")[params['id']-1]
			
			if user.cash<coin["cash"]:
				self.response.write(json_dumps_encrypt({"result":1}))
				Abuser.addUser(user, ABUSE_MEMORY_HACK)
				UserLog.append(user, ACTION_BUY_COIN, 
					{
						"status":u"복숭아 부족(어뷰징)",
						"id":coin["id"],
						"cash":coin["cash"],
						"coin":params['coin'],
						"ucash":user.cash,
						"ucoin":user.coin,
					}
				)
				return
			if coin["current_amount"]!=params['coin']: # mismatch (sync discount-time)
				self.response.write(json_dumps_encrypt({"result":2}))
				UserLog.append(user, ACTION_BUY_COIN, 
					{
						"status":u"개수 다름(어뷰징)",
						"id":coin["id"],
						"cash":coin["cash"],
						"coin":params['coin'],
						"ucash":user.cash,
						"ucoin":user.coin,
					}
				)
				return
			
			user.cash -= coin["cash"]
			user.coin += params['coin']
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			
			user.put()

			result = {
				"result":0,
				"cash":user.cash,
				"coin":user.coin,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			self.response.write(json_dumps_encrypt(result))

			StatUtil.increment(STAT_BUY_COIN.format(user.platform_id, params['id']))
			
			UserLog.append(user, ACTION_BUY_COIN, 
				{
					"status":u"성공",
					"id":coin["id"],
					"cash":coin["cash"],
					"coin":params['coin'],
					"ucash":user.cash,
					"ucoin":user.coin,
				}
			)
			
class BuyBoutHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			bout = getStorageData("bouts")[params['id']-1]

			if user.cash<bout["cash"]:
				self.response.write(json_dumps_encrypt({"result":1}))
				Abuser.addUser(user, ABUSE_MEMORY_HACK)
				UserLog.append(user, ACTION_BUY_BOUT, 
					{
						"status":u"복숭아 부족(어뷰징)",
						"id":bout["id"],
						"cash":bout["cash"],
						"bout":params['bout'],
						"ucash":user.cash,
						"ubout":user.bout,
					}
				)
				return
			if bout["current_amount"]!=params['bout']: # mismatch (sync discount-time)
				self.response.write(json_dumps_encrypt({"result":3}))
				UserLog.append(user, ACTION_BUY_BOUT, 
					{
						"status":u"개수 다름(어뷰징)",
						"id":bout["id"],
						"cash":bout["cash"],
						"bout":params['bout'],
						"ucash":user.cash,
						"ubout":user.bout,
					}
				)
				return

			user.cash -= bout["cash"]
			user.bout += params['bout']
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			
			user.put()

			result = {
				"result":0,
				"cash":user.cash,
				"bout":user.bout,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			self.response.write(json_dumps_encrypt(result))
			
			StatUtil.increment(STAT_BUY_BOUT.format(user.platform_id, params['id']))
			
			UserLog.append(user, ACTION_BUY_BOUT, 
				{
					"status":u"성공",
					"id":bout["id"],
					"cash":bout["cash"],
					"bout":params['bout'],
					"ucash":user.cash,
					"ubout":user.bout,
				}
			)

class ValidateHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			result = {}
			now = datetime.now()
			
			kind = self.request.get('kind')
			
			if kind=="userBout":
				bout = user.bout
				update_bout(user)
				if bout!=user.bout:
					user.put()
				
				result = {
					"bout":user.bout,
					"feedBoutInSec":user.feedBoutInSec(),
				}
				
			elif kind=="friendBout":
				friendUser = User.get_by_id(params['id'])
				
				result = {
					"id":params['id']
				}
				if friendUser:
					ufb = UserFriendBout.query(UserFriendBout.user_id==user.key.id(), UserFriendBout.friend_id==friendUser.key.id()).get()
					if ufb is None:
						result["boutAcceptableInSec"] = 0
						result["boutRequestableInSec"] = 0
					else:
						result["boutAcceptableInSec"] = ufb.boutAcceptableInSec(now)
						result["boutRequestableInSec"] = ufb.boutRequestableInSec(now)
				else:
					result["boutAcceptableInSec"] = INTERVAL_SEND_BOUT
					result["boutRequestableInSec"] = INTERVAL_REQUEST_BOUT
					
			elif kind=="weeklyScore":
				data = user.prevSeasonData()
				if data is None or data.rewarded:
					result = {
						"result":1,
						"weeklyExpireInSec":Season.season().weeklyExpireInSec(now),
					}
				else:
					friendIds = params['friends']
					friendUsers = ndb.get_multi([ndb.Key(User, i) for i in params['friends']], read_policy=ndb.EVENTUAL_CONSISTENCY)
					season = Season.season()
					friends, rank, existCount = getUserFriends(user, friendUsers, friendIds, season.count, True)
					
					singleReward = 0
					if rank==1:
						user.accrue_record.rank_ups[0] += 1
						singleReward = 20000
					elif rank==2:
						user.accrue_record.rank_ups[1] += 1
						singleReward = 15000
					elif rank==3:
						user.accrue_record.rank_ups[2] += 1
						singleReward = 10000
					
					user.coin += singleReward
					data.rewarded = True
					user.put()
					
					result = {
						"result":0,
						"weeklyExpireInSec":season.weeklyExpireInSec(now),
						"score":sum(data.scores),
						"rank":rank,
						"friends":friends,
						"coin":user.coin,
						"rewardCoin":singleReward,
						"inbox":UserInbox.unreadCount(user),
					}
						
			if EventFactory.run(user, session, params, TRIGGER_LOGIN) > 0:
				user.put()
				
			result["inbox"] = UserInbox.unreadCount(user)>0
			result["alerts"] = popAlerts(user)
			
			self.response.write(json_dumps_encrypt(result))
			
			if kind=="weeklyScore":
				UserLog.append(user, ACTION_SEASON_REWARD, 
					{
						"rank":result["rank"],
						"score":result["score"],
						"coin":result["rewardCoin"],
						"ucoin":result['coin'],
						"friendCount":len(result["friends"]),
					}
				)
				
class PrivacyHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			kind = self.request.get('kind')
			if kind=="push":
				if bool(params.get('blocked')):
					user.push_blocked = True
					user.push_token = None
				else:
					user.push_blocked = False
					user.push_token = params['pushToken']
				update_push_token(user)
			elif kind=='bout':
				user.bout_blocked = bool(params.get('blocked'))
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			
			user.put()
			
			result = {
				"blocked":bool(params.get('blocked')),
				"kind":kind,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			
			self.response.write(json_dumps_encrypt(result))
			
			UserLog.append(user, ACTION_BLOCK, 
				{
					"blocks":[user.push_blocked,user.bout_blocked], 
				}
			)

class TutorialHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			obtainCash = 0
			tutorial = int(params["tutorial"])
			if not tutorial in user.tutorials:
				user.tutorials.append(tutorial)
				user.tutorials.sort()
				if params.get("obtainCash"):
					user.cash += params["obtainCash"]
					obtainCash = params["obtainCash"]
				user.put()
			
			result = {
				"obtainCash":obtainCash,
				"cash":user.cash,
				"tutorials":user.tutorials,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			self.response.write(json_dumps_encrypt(result))
			
			UserLog.append(user, ACTION_TUTORIAL, 
				{
					"cash":obtainCash,
					"ucash":user.cash,
					"tutorials":user.tutorials
				}
			)

class ReadyHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():	
			user.author_key = random.randrange(0,10000)
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			
			user.put()

			result = {
				"authorKey":user.author_key,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			
			self.response.write(json_dumps_encrypt(result))

			UserLog.append(user, ACTION_READY, 
				{
					"authorKey":user.author_key,
				}
			)
			
class BoastHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			if user.author_key!=params['authorKey']:
				# add abuser
				self.error(400) # Bad Request
				UserLog.append(user, ACTION_BOAST, 
					{
						"status":u"인증키에러",
						"authorKey":user.author_key,
						"inputKey":params['authorKey'],
						"udpoint":0,
						"dpoint":0,
					}
				)
				return

			if params['id']==user.key.id():
				# add abuser
				self.error(400) # Bad Request
				return
			
			user.author_key = random.randrange(0,10000)
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			
			user.put()
			
			result = {
				"id":params['id'],
				"inbox":UserInbox.unreadCount(user)>0,
				"dailyPoint":0,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}

			self.response.write(json_dumps_encrypt(result))

			StatUtil.increment(STAT_BOAST.format(user.platform_id))
			
			UserLog.append(user, ACTION_BOAST, 
				{
					"status":u"성공",
					"authorKey":user.author_key,
					"inputKey":params['authorKey'],
					"udpoint":0,
					"dpoint":0,
				}
			)

class SendBoutHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			friend_id = params['id']

			friendUser = User.get_by_id(friend_id)
			if friendUser is None:
				result = {
					"result":2,
					"id":friend_id
				}
				self.response.write(json_dumps_encrypt(result))
				UserLog.append(user, ACTION_SEND_BOUT, 
					{
						"status":u"친구 못 찾음",
						"id":friend_id,
						"udpoint":0,
						"dpoint":0,
					}
				)
				return

			result = None
			now = datetime.now()
						
			ufb = UserFriendBout.query(UserFriendBout.user_id==user.key.id(), UserFriendBout.friend_id==friendUser.key.id()).get()
			if ufb is None:
				UserFriendBout(user_id=user.key.id(), friend_id=friendUser.key.id(), sent_at=now).put()
			else:
				ufbis = ufb.boutAcceptableInSec(now)
				if ufbis>ALLOWANCE_SYNC_SECONDS:
					result = {
						"result":1,
						"id":friend_id,
						"boutAcceptableInSec":ufbis,
					}
				else:
					ufb.sent_at=now	
					ufb.put()			
			
			if result is None:				
				user.put()
			
				addInbox(friendUser, u"선물", u"%s" % user.name, REWARD_BOUT, 1, False, FROM_FRIEND)
				
				achievements = []
				user.accrue_record.present += 1
				acvs = checkAchievement(user, 1)
				for achievement in acvs:
					r,a = obtainReward(user, achievement["reward"], achievement["amount"])
				achievements += acvs

				result = {
					"result":0,
					"id":friend_id,
					"dailyPoint":0,
					"boutAcceptableInSec":INTERVAL_SEND_BOUT,
					"inbox":UserInbox.unreadCount(user)>0,
					"alerts":popAlerts(user),
					"achievements":achievements,
				}

			self.response.write(json_dumps_encrypt(result))

			if result["result"]==0:
				StatUtil.increment(STAT_SEND_BOUT.format(user.platform_id))
			
			UserLog.append(user, ACTION_SEND_BOUT, 
				{
					"status":u"성공" if result["result"]==0 else u"실패(시간제한)",
					"id":friend_id,
					"udpoint":0,
					"dpoint":0,
				}
			)

class RequestBoutHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			friend_id = params['id']

			friendUser = User.get_by_id(friend_id)
			if friendUser is None:
				result = {
					"result":2,
					"id":friend_id
				}
				self.response.write(json_dumps_encrypt(result))
				UserLog.append(user, ACTION_REQUEST_BOUT, 
					{
						"status":u"친구 못 찾음",
						"id":friend_id,
					}
				)
				return

			result = None
			now = datetime.now()
						
			ufb = UserFriendBout.query(UserFriendBout.user_id==user.key.id(), UserFriendBout.friend_id==friendUser.key.id()).get()
			if ufb is None:
				UserFriendBout(user_id=user.key.id(), friend_id=friendUser.key.id(), requested_at=now).put()
			else:
				ufbis = ufb.boutRequestableInSec(now)
				if ufbis>ALLOWANCE_SYNC_SECONDS:
					result = {
						"result":1,
						"id":friend_id,
						"boutRequestableInSec":ufbis,
					}
				else:
					ufb.requested_at=now	
					ufb.put()			
			
			if result is None:
				result = {
					"result":0,
					"id":friend_id,
					"boutRequestableInSec":INTERVAL_REQUEST_BOUT,
					"inbox":UserInbox.unreadCount(user)>0,
					"alerts":popAlerts(user),
				}

			self.response.write(json_dumps_encrypt(result))

			if result["result"]==0:
				StatUtil.increment(STAT_REQ_BOUT.format(user.platform_id))
				
			UserLog.append(user, ACTION_REQUEST_BOUT, 
				{
					"status":u"성공" if result["result"]==0 else u"실패(시간제한)",
					"id":friend_id,
				}
			)
		
class InviteHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			friend_id = abs(int(params['id']))
			
			if friend_id==user.key.id():
				# add abuser
				self.error(400) # Bad Request
				return
			if User.get_by_id(friend_id) is not None:
				self.response.write(json_dumps_encrypt({"result":2, "id":params['id']}))
				UserLog.append(user, ACTION_INVITE_APP, 
					{
						"status":u"친구 못찾음",
						"id":friend_id,
						"count":user.accrue_record.invite,
						"reward":-1,
						"rewardArg":0
					}
				)
				return
			
			insec = 0
			userInvite = UserInvite.query(UserInvite.user_id==user.key.id(), UserInvite.friend_id==friend_id, ancestor=UserInvite.ancestorKey(user)).get()
			if userInvite is None:
				userInvite = UserInvite(parent=UserInvite.ancestorKey(user), user_id=user.key.id(), friend_id=friend_id)
			else:
				insec = userInvite.inviteAcceptableInSec()

			reward = None
			rewardArg = 0
				
			if insec==0:
				userInvite.put()
								
				user.bout += 1
				user.accrue_record.invite += 1
				achievements = checkAchievement(user, 2, withObtain=True)
				
				for ir in INVITE_REWARDS:
					if user.accrue_record.invite==ir:
						obtainReward(user, INVITE_REWARDS[ir]['reward'], INVITE_REWARDS[ir]['rewardArg'], True)
						break

				EventFactory.run(user, session, params, TRIGGER_LOGIN)
				EventFactory.run(user, session, params, TRIGGER_INVITE)
				
				user.put()
				
				result = {
					"result":0,
					"id":params['id'],
					"cash":user.cash,
					"coin":user.coin,
					"bout":user.bout,
					"dailyPoint":0,
					"invite":user.accrue_record.invite,
					"remainInvite":user.remainiInvite(user.accrue_record.invite),
					"inviteRewards":user.invite_rewards,
					"invitableInSec":INTERVAL_INVITE,					
					"missions":achievements,
					"inbox":UserInbox.unreadCount(user)>0,
					"alerts":popAlerts(user),
				}
				if reward:
					result['reward'] = reward
					result['rewardArg'] = rewardArg
			else:
				result = {
					"result":1,
					"id":params['id'],
					"invitableInSec":insec
				}
			self.response.write(json_dumps_encrypt(result))
			
			StatUtil.increment(STAT_INVITE.format(user.platform_id))
		
			UserLog.append(user, ACTION_INVITE_APP, 
				{
					"status":u"성공" if result["result"]==0 else u"실패(시간제한)",
					"id":params['id'],
					"count":user.accrue_record.invite,
					"reward":reward,
					"rewardArg":rewardArg,
					"udpoint":0,
					"dpoint":0,
				}
			)

class MissionsHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			todayMission = Mission.todayMission()
			update_mission_record(user, todayMission)
			
			d3, w3, achievements = getUserMissions(user, todayMission)
			
			EventFactory.run(user, session, params, TRIGGER_LOGIN)
			
			user.put()
			
			result = {
				"dailyMissions":d3,
				"weeklyMissions":w3,
				"achievements":achievements,
				"weeklyExpireInSec":todayMission.weeklyExpireInSec(datetime.now()),
				"dailyExpireInSec":todayMission.dailyExpireInSec(datetime.now()),
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			self.response.write(json_dumps_encrypt(result))

class GameStartHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():	
			session["game_result_key"] = random.randrange(0,10000)
			user.game_result_key = session["game_result_key"]
			if user.bout==MAX_FEED_BOUT:
				user.bout_feed_at = datetime.now()
			
			game = params["game"]
			needCoin,items = user.availableItems(params["game"], params["items"])
			consummableCount = sum([(1 if i>0 else 0) for i in items[:4]])
						
			todayMission = Mission.todayMission()
						
			if user.bout<=0:
				self.error(400) # Bad Request
				return
			user.coin -= needCoin
			user.bout -= 1				
			update_mission_record(user, todayMission)
			
			user.put()
			
			result = {
				"result":0,
				"coin":user.coin,
				"bout":user.bout,
				"feedBoutInSec":user.feedBoutInSec(),
				"authorKey":session["game_result_key"],
				"game":game,				
				"dailyMissions":todayMission.daily_missions,
				"weeklyMissions":todayMission.weekly_missions,
				"items":items,				
				"dailyPoint":0,
			}
			self.response.write(json_dumps_encrypt(result))

			for i in range(len(items)):
				if items[i]>0:
					StatUtil.increment(STAT_USE_ITEM.format(user.platform_id, game, i))
			
			UserLog.append(user, ACTION_GAME_START, 
				{
					"authorKey":result["authorKey"],
					"game":game,					
					"coin":needCoin,
					"ucoin":user.coin,
					"ucash":user.cash,
					"bout":1,
					"ubout":user.bout,
					"items":items,
					"udpoint":0,
				}
			)
			
class GameResultHandler(SessionHandler):
	def post(self):
		# 클라이언트 -> 점수+보너스점수+획득코인
		# 서버 -> 장착캐릭터, 레벨,경험치, 지난주 최고 점수(리더보드에서)
		for params, user, session in self.before_filter():	
			### for stress - testing
			if params and params.get("stress-test"):
				if (session.get("game_result_key") != params['authorKey']):
					logging.info("confused!!! %s, %s" % (session.get("game_result_key"),params['authorKey'],))
				user.game_result_key = params['authorKey']
				session["game_result_key"] = params['authorKey']
			###
			
			if (not params.get("resend") and (user.game_result_key != params['authorKey']) and (session.get("game_result_key") != params['authorKey'])):
				logging.info("game_result_key:%s, authorKey:%s, session:%s" % (user.game_result_key, params['authorKey'], session.get("game_result_key"),))
				self.error(400) # Bad Request
				return
			
			season = Season.season()
			seasonData = user.seasonData(season.count)
			prevSeasonData = user.prevSeasonData(season.count)
			
			game = params["game"]
			score = params["score"]
			combo = params["combo"]
			fever = params["fever"]
			bonusScore = params["bonusScore"]
			obtainCoin = params["obtainCoin"]
			obtainExp = params["obtainExp"]
			
			newRecord = False
			if score+bonusScore > seasonData.scores[game]:
				seasonData.scores[game] = score+bonusScore
				newRecord = True

			user.accrue_record.play += 1
			user.accrue_record.best_score = max(sum(seasonData.scores), user.accrue_record.best_score)
			user.coin += obtainCoin
			
			friendIds = params['friends']
			friendUsers = ndb.get_multi([ndb.Key(User, i) for i in params['friends']], read_policy=ndb.EVENTUAL_CONSISTENCY)
			friends, rank, existCount = getUserFriends(user, friendUsers, friendIds, season.count, False)

			seasonData.ranks[game] = rank
									
			missions = checkMission(user, params, TRIGGER_GAME_RESULT)
			for mission in missions:
				obtainReward(user, mission["reward"], mission["amount"])
						
			session["game_result_key"] = random.randrange(0,10000)
			user.game_result_key = session["game_result_key"]
			
			EventFactory.run(user, session, params, TRIGGER_GAME_RESULT)
			
			user.put()
			
			result = {
				"newRecord":newRecord,
				"cash":user.cash,
				"coin":user.coin,
				"bout":user.bout,
				"score":score,
				"game":game,
				"combo":combo,
				"fever":fever,
				"obtainCoin":obtainCoin,
				"obtainExp":obtainExp,
				"bonusScore":bonusScore,				
				"prevScore":prevSeasonData.scores[game] if prevSeasonData else 0,
				"feedBoutInSec":user.feedBoutInSec(),				
				"friends":friends,
				"dailyPoint":0,
				"missions":missions,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}
			
			self.response.write(json_dumps_encrypt(result))

			StatUtil.increment(STAT_GAME.format(user.platform_id, game))
			
			del result["inbox"], result["alerts"], result["friends"], result["feedBoutInSec"]
			UserLog.append(user, ACTION_GAME_RESULT, result)

class ContinueHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			PRICE = 3
			if user.cash>=PRICE:
				user.cash -= PRICE
				result = {
					"result":0,
					"cash":user.cash
				}
				user.put()
			else:
				result = {
					"result":1,
					"cash":user.cash
				}
			self.response.write(json_dumps_encrypt(result))

			StatUtil.increment(STAT_CONTINUE.format(user.platform_id))
			
			UserLog.append(user, ACTION_USE_CONTINUE, 
				{
					"status":u"성공" if result["result"]==0 else u"복숭아부족",
					"cash":PRICE,
					"ucash":user.cash,
				}
			)
			
class InboxesHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			inboxes = []
			tooOldInboxes = []
			count = 0
			for inbox in UserInbox.query(UserInbox.user_id==user.key.id(), ancestor=UserInbox.ancestorKey(user)).order(-UserInbox.created_at).fetch():
				if count>50:
					tooOldInboxes.append(inbox.key)
				else:
					inboxes.append(inbox.serialize())
				count += 1
			ndb.delete_multi(tooOldInboxes)
			
			inboxes = sorted(inboxes, key=itemgetter('created_at')) 
			for inbox in inboxes:
				del inbox["created_at"]

			result = {
				"inboxes":inboxes,
				"inbox":UserInbox.unreadCount(user)>0,
				"alerts":popAlerts(user),
			}

			self.response.write(json_dumps_encrypt(result))

class ConfirmInboxHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			if params.get('id') == 0: # 모두 받기
				inboxes = UserInbox.query(UserInbox.user_id==user.key.id(), ancestor=UserInbox.ancestorKey(user)).fetch()

				if len(inboxes) == 0:
					self.error(400) # Bad Request
					return
				
				# 통계에 보상항목들 추가, 여기엔 캐릭터가 들어오면 안됨(캐릭터는, 초대보상으로만 가능)
				REWARDS = [0,0,0]
				rewards = []
				for inbox in inboxes[:50]:
					rewards.append(
						{
							"reward":inbox.reward,
							"amount":inbox.amount
						}
					)
					r,a = obtainReward(user, inbox.reward, inbox.amount)
					REWARDS[r]+=a
				user.put()
				UserInbox.deleteAll(user, inboxes)

				result = {
					"bout":user.bout,
					"cash":user.cash,
					"coin":user.coin,
					"rewards":rewards,
				}
				self.response.write(json_dumps_encrypt(result))
				
				for i in range(len(REWARDS)): # 친구가 선물해준것도 포함되는데.... (선물항목을 빼는 식으로 관리자가 판단해야함)
					if REWARDS[i]>0:
						StatUtil.increment(STAT_REWARDS.format(user.platform_id, i), REWARDS[i])
				
				UserLog.append(user, ACTION_CONFIRM_INBOX, 
					{
						"ucash":user.cash,
						"ucoin":user.coin,
						"ubout":user.bout,
						"rewards":rewards,
					}
				)
				return
			
			inbox = UserInbox.get_by_id(params['id'], parent=UserInbox.ancestorKey(user))
			if inbox is None:
				self.error(400) # Bad Request
				return

			rewards = []
			rewards.append(
				{
					"reward":inbox.reward,
					"amount":inbox.amount
				}
			)
			obtainReward(user, inbox.reward, inbox.amount, True)
			user.put()
			UserInbox.delete(user, inbox)
			
			result = {
				"bout":user.bout,
				"cash":user.cash,
				"coin":user.coin,
				"rewards":rewards,
			}
			
			self.response.write(json_dumps_encrypt(result))

			UserLog.append(user, ACTION_CONFIRM_INBOX, 
				{
					"ucash":user.cash,
					"ucoin":user.coin,
					"ubout":user.bout,
					"rewards":rewards,
				}
			)
			
class UnregisterHandler(SessionHandler):
	def post(self):
		for params, user, session in self.before_filter():
			def user_models():
				for cls in UserModel.__subclasses__():
					if cls == UserLog:
						continue
					yield cls

			def deleteUserData():
				for cls in user_models():
					objs = cls.query(cls.user_id==user.key.id(), ancestor=cls.ancestorKey(user)).fetch(keys_only=True)
					ndb.delete_multi(objs)
				user.key.delete()
			
			deleteUserData()
			
			self.response.write(json_dumps_encrypt({"result":0}))

			StatUtil.increment(STAT_UNREGISTER.format(user.platform_id))
			
			UserLog.append(user, ACTION_UNREGISTER, 
				{
					"ucash":user.cash,
					"ucoin":user.coin,
					"ubout":user.bout,
				}
			)

class DownloadAssetHandler(blobstore_handlers.BlobstoreDownloadHandler):
	def get(self, assetKey):
		resource = str(urllib.unquote(assetKey))
		blob_info = blobstore.BlobInfo.get(resource)
		self.send_blob(blob_info)

class FetchDataHandler(webapp2.RequestHandler):
	def get(self):
		passwd = self.request.get("passwd")
		if passwd != '39jf2nnn9':
			self.error(400) # Bad Request
			return
		
		more = False
		datas = []
		cursor = Cursor(urlsafe=self.request.get('cursor')) if bool(self.request.get('cursor')) else None
		count = int(self.request.get("count")) if self.request.get("count") else 0
		if self.request.get('kind')=='push':
			min_at = datetime.strptime(cgi.escape(self.request.get('upd_at')), "%Y-%m-%d %H:%M:%S")
			
			if APP_DEBUG:
				results, cursor, more = User.query(UpdatedPushToken.updated_at>=min_at).fetch_page(count, start_cursor=cursor)
				for r in results:
					datas.append(
						{
							"id":r.key.id(),
							"token":r.push_token,
							"platform":r.platform_id,
							"appver":r.application_version,
							"assver":r.asset_version,
							"invalid":False,
						}
					)
			else:
				results, cursor, more = UpdatedPushToken.query(UpdatedPushToken.updated_at>=min_at).fetch_page(count, start_cursor=cursor)
				for r in results:
					datas.append(
						{
							"id":r.key.id(),
							"token":r.push_token,
							"platform":r.platform_id,
							"appver":r.application_version,
							"assver":r.asset_version,
							"invalid":r.invalid,
						}
					)
		elif self.request.get('kind')=='reserved':
			step = int(self.request.get('step'))
			results = ReservedPush.query(ReservedPush.step==step, ancestor=ReservedPush.ancestorKey()).order(-ReservedPush.created_at).fetch()
			for r in results:
				datas.append(
					{
						"id":r.key.id(),
						"platform":r.platform_id,
						"appver":r.application_version,
						"assver":r.asset_version,
						"reserved_at":replace_timezone(r.reserved_at,'Asia/Seoul').strftime('%Y-%m-%d %H:%M:%S') if r.reserved_at else None,
						"deadline_at":replace_timezone(r.deadline_at,'Asia/Seoul').strftime('%Y-%m-%d %H:%M:%S') if r.deadline_at else None,
						"title":r.title,
						"message":r.message,
						"badge":r.badge,
					}
				)
				
		result = {
			"more":more,
			"cursor":cursor.urlsafe() if cursor else None,
			"datas":datas,
			"fetching_at":datetime.now().strftime('%Y-%m-%d %H:%M:%S')
		}
		self.response.out.write(json.dumps(result))
		
class ReserveStatusHandler(webapp2.RequestHandler):
	def get(self):
		passwd = self.request.get("passwd")
		if passwd != '39jf2nnn9':
			self.error(400) # Bad Request
			return
		
		pid = int(self.request.get("id"))
		status = int(self.request.get("status"))
		rp = ReservedPush.get_by_id(pid, parent=ReservedPush.ancestorKey())
		if rp is None:
			result = {
				"result":1,
			}
		else:
			rp.step=status
			rp.put()
			result = {
				"result":0,
			}
		self.response.out.write(json.dumps(result))

class ReviewHandler(webapp2.RequestHandler):
	def get(self):
		userId = int(self.request.get("id"))
		user = User.get_by_id(userId)
		if user is None:
			self.error(400)
			return
		
		if user.review is False:
			user.review = True
			addInbox(user, u"운영자님께서 코인 %d개를 지급하였습니다" % (2000,), u"운영자님께서 코인 %d개를 지급하였습니다" % (2000,), REWARD_COIN, 2000, False, FROM_ADMIN)
			user.put()
			
		if user.platform_id==PLATFORM_IOS:
			self.redirect(STORE_URL_IOS)
			return
		elif user.platform_id==PLATFORM_PLAYSTORE:
			self.redirect(STORE_URL_PLAYSTORE)
			return
		self.response.write(json_dumps_encrypt({"result":1}))
		
		StatUtil.increment(STAT_REVIEW.format(user.platform_id))
			
###############################################################################
## Test
###############################################################################

class TestHandler(SessionHandler):	
	def post(self):
		result = {}
		self.response.write(json_dumps_encrypt(result))

